﻿using ModelHasher.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ModelHasher.Utils
{
    public class HashingUtil
    {
        public class HasherProgress
        {
            public long TotalBytesRead { get; private set; }
            public long TotalBytes { get; private set; }

            public HasherProgress(long totalBytesRead, long totalBytes)
            {
                TotalBytesRead = totalBytesRead;
                TotalBytes = totalBytes;
            }
        }

        public static async Task<string> CalculateFileHashAsync(
            HashAlgo hashAlgo,
            string filePath,
            CancellationToken cancellationToken = default(CancellationToken),
            IProgress<HasherProgress> progress = null,
            int bufferSize = 1000000 * 16,
            bool lowerCase = false,
            bool removeDash = true)
        {
            using (var stream = File.OpenRead(filePath))
            {
                return HashBytesToString(await CalculateHashAsync(hashAlgo, stream, cancellationToken, progress, bufferSize), lowerCase, removeDash);
            }
        }

        public static async Task<byte[]> CalculateHashAsync(
            HashAlgo algo, Stream stream,
            CancellationToken cancellationToken = default(CancellationToken),
            IProgress<HasherProgress> progress = null,
            int bufferSize = 1000000 * 16)
        {
            byte[] readAheadBuffer, buffer, hash;
            int readAheadBytesRead, bytesRead;
            long size, totalBytesRead = 0;
            size = stream.Length;
            readAheadBuffer = new byte[bufferSize];
            readAheadBytesRead = await stream.ReadAsync(readAheadBuffer, 0, readAheadBuffer.Length, cancellationToken);
            totalBytesRead += readAheadBytesRead;
            using (var hashAlgorithm = CreateHashAlgorithm(algo))
            {
                do
                {
                    bytesRead = readAheadBytesRead;
                    buffer = readAheadBuffer;
                    readAheadBuffer = new byte[bufferSize];
                    readAheadBytesRead = await stream.ReadAsync(readAheadBuffer, 0,
                        readAheadBuffer.Length, cancellationToken);
                    totalBytesRead += readAheadBytesRead;

                    if (readAheadBytesRead == 0)
                        hashAlgorithm.TransformFinalBlock(buffer, 0, bytesRead);
                    else
                        hashAlgorithm.TransformBlock(buffer, 0, bytesRead, buffer, 0);
                    if (progress != null)
                        progress.Report(new HasherProgress(totalBytesRead, size));
                    if (cancellationToken.IsCancellationRequested)
                        cancellationToken.ThrowIfCancellationRequested();
                } while (readAheadBytesRead != 0);
                return hash = hashAlgorithm.Hash;
            }
        }

        public static string HashBytesToString(byte[] bytes, bool lowerCase = false, bool removeDash = false)
        {
            string Result = "";

            Result = BitConverter.ToString(bytes);

            if (removeDash)
                Result = Result.Replace("-", "");

            if (lowerCase)
                Result = Result.ToLower();

            return Result;
        }

        public static HashAlgorithm CreateHashAlgorithm(HashAlgo hashAlgo)
        {
            switch (hashAlgo)
            {
                case HashAlgo.MD5:
                    return MD5.Create();
                case HashAlgo.SHA1:
                    return SHA1.Create();
                case HashAlgo.SHA256:
                    return SHA256.Create();
                case HashAlgo.SHA384:
                    return SHA384.Create();
                case HashAlgo.SHA512:
                    return SHA512.Create();
                case HashAlgo.RIPEMD160:
                    return RIPEMD160.Create();
                default:
                    return null;
            }
        }
    }
}
